//
// Created by Administrator on 2016/8/31.
//

#include "Model.h"

using namespace std;
using namespace ZainGL;
using namespace ZainTL;
using namespace glm;

Model::Model(const char *path, bool bCombineMesh, GLuint maxInstanceNum, unordered_map<string, FileDatas> *bufferMap) {
    Assimp::Importer importer;
    const aiScene *scene = nullptr;
    if (bufferMap == nullptr) {
        scene = importer.ReadFile(path,
                                  aiProcess_Triangulate | aiProcess_FlipUVs |
                                  aiProcess_GenSmoothNormals);
    } else {
        auto mapIter = bufferMap->find(path);
        if (mapIter != bufferMap->end()) {
            FileDatas fileData = mapIter->second;
            scene = importer.ReadFileFromMemory(fileData.data, fileData.dataLen,
                                                aiProcess_Triangulate | aiProcess_FlipUVs |
                                                aiProcess_GenSmoothNormals | aiProcess_OptimizeGraph);
        }
    }
    if (!scene || scene->mFlags == AI_SCENE_FLAGS_INCOMPLETE || !scene->mRootNode) {
        cout << "ERROR::ASSIMP::" << importer.GetErrorString() << endl;
        return;
    }
    aiMatrix4x4 rootTrans = scene->mRootNode->mTransformation;
    mTransform = aiMatrix4x4ToGlm(rootTrans);

    processNode(scene->mRootNode, scene, mTransform, bCombineMesh, maxInstanceNum, bufferMap);
    if (bCombineMesh) {
        bindDataGL(0);
    }
}

void Model::processNode(aiNode *node, const aiScene *scene, glm::mat4 parentAbsoluteTrans, bool bCombine, GLuint maxInstanceNum, std::unordered_map<std::string, ZainTL::FileDatas> *bufferMap) {
    int nodeMeshes = node->mNumMeshes;

    mat4 currentAbsoluteTrans = parentAbsoluteTrans;
    if (node != scene->mRootNode) {
        aiMatrix4x4 nodeTrans = node->mTransformation;//The transformation relative to the node's parent.
        mat4 nodeTransGLM = aiMatrix4x4ToGlm(nodeTrans);
        currentAbsoluteTrans = parentAbsoluteTrans * nodeTransGLM;
    }

    for (int i = 0; i < nodeMeshes; i++) {
        aiMesh *aimesh = scene->mMeshes[node->mMeshes[i]];
        Model *mesh = bCombine ? this : new Model();

        //Vertices--Position??Normal??TexCoord
        GLuint verticesNum = aimesh->mNumVertices;

        mesh->mVertices.reserve(mesh->mVertices.size() + verticesNum);
        for (int i = 0; i < verticesNum; i++) {
            Vertex vertex;

            vertex.Position.x = aimesh->mVertices[i].x;
            vertex.Position.y = aimesh->mVertices[i].y;
            vertex.Position.z = aimesh->mVertices[i].z;

            vertex.Normal.x = aimesh->mNormals[i].x;
            vertex.Normal.y = aimesh->mNormals[i].y;
            vertex.Normal.z = aimesh->mNormals[i].z;

            vertex.Position = mat3(currentAbsoluteTrans) * vertex.Position;
            vertex.Normal = mat3(currentAbsoluteTrans) * vertex.Normal;

            if (aimesh->mTextureCoords[0]) {
                vertex.TexCoord.x = aimesh->mTextureCoords[0][i].x;
                vertex.TexCoord.y = aimesh->mTextureCoords[0][i].y;
            } else {
                vertex.TexCoord = vec2(0, 0);
            }

            mesh->mVertices.push_back(vertex);
        }

        //Indices
        unsigned int meshIndices = aimesh->mNumFaces * 3;
        mesh->mIndices.reserve(mesh->mIndices.size() + meshIndices);
        for (GLuint i = 0; i < aimesh->mNumFaces; i++) {
            aiFace meshFace = aimesh->mFaces[i];
            mesh->mIndices.push_back(meshFace.mIndices[0]);
            mesh->mIndices.push_back(meshFace.mIndices[1]);
            mesh->mIndices.push_back(meshFace.mIndices[2]);
        }

        //Vertices--BoneIDs
        GLuint bonesNum = aimesh->mNumBones;
        for (GLuint i = 0; i < bonesNum; ++i) {
            GLuint boneWeightsNum = aimesh->mBones[i]->mNumWeights;
            for (GLuint j = 0; j < boneWeightsNum; j++) {
                GLuint vertexID = aimesh->mBones[i]->mWeights[j].mVertexId;
                GLfloat weight = aimesh->mBones[i]->mWeights[j].mWeight;
                for (GLuint k = 0; k < BONES_NUM_PER_VTX; ++k) {
                    if (mesh->mVertices[vertexID].BoneWeights[k] == 0.0) {
                        mesh->mVertices[vertexID].BoneIDs[k] = i;
                        mesh->mVertices[vertexID].BoneWeights[k] = weight;
                        break;
                    }
                }
            }
        }

        //Material
        //A mesh does use only a single material. If an imported model uses multiple materials, the import splits up the mesh. Use this value as index into the scene's material list.
        aiMaterial *meshMaterial = scene->mMaterials[aimesh->mMaterialIndex];
        for (uint8_t type = aiTextureType_DIFFUSE; type <= aiTextureType_REFLECTION; ++type) {
            GLuint textureCount = meshMaterial->GetTextureCount((aiTextureType) type);
            for (GLuint i = 0; i < textureCount; i++) {
                aiString path;
                meshMaterial->GetTexture((aiTextureType) type, i, &path);
                string texturePath = path.C_Str();
                regex r("\\\\");
                texturePath = regex_replace(texturePath, r, "/");
                if (texturePath.find_first_of("..") == 0) {//if it is relativePath to model
                    string mFolder = mesh->mPath.substr(0, mesh->mPath.find_last_of("/"));
                    texturePath = mFolder.size() == 1 ? mFolder.append(texturePath.substr(2)) : mFolder.substr(0,
                                                                                                               mFolder.find_last_of(
                                                                                                                       "/")).append(
                            texturePath.substr(2));
                }
                Texture *texture = loadTexture(texturePath.c_str(), bufferMap);
                if (texture != nullptr) {
                    texture->type = (aiTextureType) type;
                    mesh->mTextures.push_back(texture);
                }
            }
        }

        if (!bCombine) {
            mesh->mTransform = currentAbsoluteTrans;
            mesh->bindDataGL(maxInstanceNum);
            mModels.push_back(mesh);
        }
    }

    int childrenNodes = node->mNumChildren;
    for (int i = 0; i < childrenNodes; i++) {
        processNode(node->mChildren[i], scene, currentAbsoluteTrans, false, 0, bufferMap);
    }
}

glm::mat4 Model::aiMatrix4x4ToGlm(const aiMatrix4x4 &from) {
    glm::mat4 to;

    to[0][0] = (GLfloat) from.a1;
    to[0][1] = (GLfloat) from.b1;
    to[0][2] = (GLfloat) from.c1;
    to[0][3] = (GLfloat) from.d1;
    to[1][0] = (GLfloat) from.a2;
    to[1][1] = (GLfloat) from.b2;
    to[1][2] = (GLfloat) from.c2;
    to[1][3] = (GLfloat) from.d2;
    to[2][0] = (GLfloat) from.a3;
    to[2][1] = (GLfloat) from.b3;
    to[2][2] = (GLfloat) from.c3;
    to[2][3] = (GLfloat) from.d3;
    to[3][0] = (GLfloat) from.a4;
    to[3][1] = (GLfloat) from.b4;
    to[3][2] = (GLfloat) from.c4;
    to[3][3] = (GLfloat) from.d4;

    return to;
}

Model::Model() {

}

void
Model::draw(std::vector<glm::mat4> *const instanceTransforms) {
    if (mModels.size() == 0) {
        EntityObject::draw(instanceTransforms);
    } else {
        for_each(mModels.cbegin(), mModels.cend(), [&](Model *modelIter) {
            modelIter->draw(instanceTransforms);
        });
    }
}

void Model::bindDataGL(GLuint maxInstanceNum) {
    glGenVertexArrays(1, &mVAO);
    glBindVertexArray(mVAO);

    GLuint VBO;
    GLsizei vertexByte = sizeof(Vertex);
    glGenBuffers(1, &VBO);
    glBindBuffer(GL_ARRAY_BUFFER, VBO);
    glBufferData(GL_ARRAY_BUFFER, mVertices.size() * vertexByte, &mVertices[0], GL_STATIC_DRAW);

    GLuint EBO;
    glGenBuffers(1, &EBO);
    glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);
    glBufferData(GL_ELEMENT_ARRAY_BUFFER, mIndices.size() * sizeof(GLuint), &mIndices[0],
                 GL_STATIC_DRAW);

    GLuint layout=0;
    // Vertex Positions
    glEnableVertexAttribArray(layout);
    glVertexAttribPointer(layout++, 3, GL_FLOAT, GL_FALSE, vertexByte, (GLvoid *) 0);
    // Vertex Normals
    glEnableVertexAttribArray(layout);
    glVertexAttribPointer(layout++, 3, GL_FLOAT, GL_FALSE, vertexByte, (GLvoid *) offsetof(Vertex, Normal));
    // Vertex Texture Coords
    glEnableVertexAttribArray(layout);
    glVertexAttribPointer(layout++, 2, GL_FLOAT, GL_FALSE, vertexByte, (GLvoid *) offsetof(Vertex, TexCoord));
    // BoneIDs
    glEnableVertexAttribArray(layout);
    glVertexAttribIPointer(layout++, 4, GL_INT, vertexByte, (GLvoid *) offsetof(Vertex, BoneIDs));
    // BoneWeights
    glEnableVertexAttribArray(layout);
    glVertexAttribPointer(layout++, 4, GL_FLOAT, GL_FALSE, vertexByte, (GLvoid *) offsetof(Vertex, BoneWeights));

    //InstanceTransformations
    glGenBuffers(1, &mIBO);
    glBindBuffer(GL_ARRAY_BUFFER, mIBO);
    glBufferData(GL_ARRAY_BUFFER, maxInstanceNum * sizeof(mat4), NULL, GL_DYNAMIC_DRAW);
    for (GLuint matRow = layout; matRow < layout + 4; ++matRow) {
        glEnableVertexAttribArray(matRow);
        glVertexAttribPointer(matRow, 4, GL_FLOAT, GL_FALSE, sizeof(mat4),
                              (GLvoid *) ((matRow - layout) * sizeof(vec4)));
        glVertexAttribDivisor(matRow, 1);
    }

    glBindVertexArray(0);
}

void Model::renderToShadowMap(vector<mat4> *const instanceTransforms) {
    if (mModels.size() == 0) {
        EntityObject::renderToShadowMap(instanceTransforms);
    } else {
        for_each(mModels.cbegin(), mModels.cend(), [&](Model *modelIter) {
            modelIter->renderToShadowMap(instanceTransforms);
        });
    }
}
